﻿// PluginsListView.cs
// (c) 2016, Charles Lechasseur
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using PathCopyCopy.Settings.Core;
using PathCopyCopy.Settings.Core.Plugins;
using PathCopyCopy.Settings.Properties;

namespace PathCopyCopy.Settings.UI.UserControls
{
    /// <summary>
    /// <see cref="UserControl"/> that can be used to display an ordered list
    /// of <see cref="Plugin"/> objects. Also has an area to display a
    /// preview of the plugin's effect.
    /// </summary>
    public partial class PluginsListView : UserControl
    {
        /// Object used to access settings.
        private UserSettings settings;

        /// Whether we created the settings object.
        private bool createdSettings;

        /// Binding list of Plugins used as data source. Null if data source is not an IBindingList.
        private IBindingList dataSource;

        /// <summary>
        /// <see cref="UserSettings"/> that can be used by the list view to
        /// access settings. Must be set before certain events are called.
        /// If not set, a new object will be created.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public UserSettings Settings
        {
            get {
                if (settings == null) {
                    settings = new UserSettings();
                    createdSettings = true;
                }
                return settings;
            }
            set {
                Debug.Assert(settings == null || settings != value);
                if (settings != null && createdSettings) {
                    settings.Dispose();
                }
                settings = value;
                createdSettings = false;
            }
        }

        /// <summary>
        /// <see cref="Plugin"/>s data source.
        /// </summary>
        public object DataSource
        {
            get {
                return PluginsDataGrid.DataSource;
            }
            set {
                if (dataSource != null) {
                    dataSource.ListChanged -= PluginsDataSource_ListChanged;
                }
                PluginsDataGrid.DataSource = value;
                dataSource = value as IBindingList;
                if (dataSource != null) {
                    dataSource.ListChanged += PluginsDataSource_ListChanged;
                }
            }
        }

        /// <summary>
        /// Whether to allow user to edit the plugins' icons.
        /// </summary>
        public bool AllowUserToEditIcons
        {
            get;
            set;
        }

        /// <summary>
        /// Whether user is allowed to select multiple plugins.
        /// </summary>
        public bool MultiSelect
        {
            get {
                return PluginsDataGrid.MultiSelect;
            }
            set {
                PluginsDataGrid.MultiSelect = value;
            }
        }

        /// <summary>
        /// Number of plugins currently selected.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionCount
        {
            get {
                return PluginsDataGrid.SelectedRows.Count;
            }
        }

        /// <summary>
        /// Index of first selected plugin. If more than one plugin
        /// is selected, returns the index of the first one. If no
        /// plugin is selected, returns <c>-1</c>.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectedIndex
        {
            get {
                return PluginsDataGrid.SelectedRows.Count > 0
                    ? PluginsDataGrid.SelectedRows[0].Index
                    : -1;
            }
            set {
                PluginsDataGrid.ClearSelection();
                if (value >= 0) {
                    PluginsDataGrid.Rows[value].Selected = true;
                    PluginsDataGrid.FirstDisplayedScrollingRowIndex = value;
                }
            }
        }

        /// <summary>
        /// Index of last selected plugin. If no plugin is selected, returns <c>-1</c>.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int LastSelectedIndex
        {
            get {
                return PluginsDataGrid.SelectedRows.Count > 0
                    ? PluginsDataGrid.SelectedRows[PluginsDataGrid.SelectedRows.Count - 1].Index
                    : -1;
            }
        }

        /// <summary>
        /// Collection of selected plugins and their indexes.
        /// </summary>
        /// <seealso cref="PluginsListView.SelectedPluginInfo"/>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public IEnumerable<SelectedPluginInfo> Selection
        {
            get {
                for (int i = 0; i < PluginsDataGrid.SelectedRows.Count; ++i) {
                    var row = PluginsDataGrid.SelectedRows[i];
                    Plugin plugin = (Plugin) row.DataBoundItem;
                    yield return new SelectedPluginInfo(plugin, row.Index);
                }
            }
        }

        /// <summary>
        /// Event fired when the selection changes in the list view.
        /// </summary>
        public event EventHandler SelectionChanged;

        /// <summary>
        /// Event fired when the icon of a plugin is updated by the user.
        /// </summary>
        public event EventHandler PluginIconChanged;

        /// <summary>
        /// Constructor
        /// </summary>
        public PluginsListView()
        {
            InitializeComponent();

            // Disable the grid view's auto-creation of columns since we've entered them by hand.
            PluginsDataGrid.AutoGenerateColumns = false;
        }

        /// <summary>
        /// Returns a textual description to ease debugging.
        /// </summary>
        /// <returns>Textual description of list view.</returns>
        public override string ToString()
        {
            return !String.IsNullOrEmpty(Name) ? Name : base.ToString();
        }

        /// <summary>
        /// Returns a stream containing the data of the given plugin's icon.
        /// </summary>
        /// <param name="plugin">Plugin whose icon to load.</param>
        /// <returns>Stream containing the icon for <paramref name="plugin"/>.
        /// </returns>
        private Stream StreamForPluginIcon(Plugin plugin)
        {
            Debug.Assert(plugin != null);
            Debug.Assert(!String.IsNullOrEmpty(plugin.IconFile));

            // Using a FileStream would keep the file locked. Load bytes
            // and use a memory stream instead.
            return new MemoryStream(File.ReadAllBytes(plugin.IconFile));
        }

        /// <summary>
        /// Loads a plugin's icon and displays it in a cell in the plugins data grid.
        /// </summary>
        /// <param name="plugin">Plugin whose icon to load.</param>
        /// <param name="rowIconCell">Cell where to display the icon.</param>
        private void LoadPluginIcon(Plugin plugin, DataGridViewImageCell rowIconCell)
        {
            Debug.Assert(plugin != null);
            Debug.Assert(rowIconCell != null);

            // Assume row will not display an Icon.
            rowIconCell.ValueIsIcon = false;

            // Check if we have an icon file to display.
            if (plugin.IconFile != null) {
                if (plugin.IconFile.Length == 0) {
                    // This indicates that we need to use the default icon.
                    rowIconCell.Value = Resources.DefaultIcon;
                } else {
                    try {
                        // Icon file to load. Check file extension for .ico,
                        // which we need to display differently.
                        Stream iconStream = StreamForPluginIcon(plugin);
                        if (Path.GetExtension(plugin.IconFile).ToLower() == ".ico") {
                            rowIconCell.ValueIsIcon = true;
                            rowIconCell.Value = new Icon(iconStream);
                        } else {
                            rowIconCell.Value = Image.FromStream(iconStream);
                        }
                    } catch (Exception ex) {
                        if (ex is InvalidOperationException || ex is ArgumentException || ex is IOException) {
                            // Failed to load the icon for some reason. Display a blank icon.
                            rowIconCell.ValueIsIcon = false;
                            rowIconCell.Value = Resources.BlankIcon;
                        } else {
                            throw;
                        }
                    }
                }
            } else {
                // No icon, set a blank image.
                rowIconCell.Value = Resources.BlankIcon;
            }
        }

        /// <summary>
        /// Called when something changes in our plugin data source.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void PluginsDataSource_ListChanged(object sender, ListChangedEventArgs e)
        {
            Debug.Assert(dataSource != null);

            // Detect changes to plugins' icon files to update our data grid.
            // (Note: when a plugin is completely replaced, an ItemChanged is triggered
            // but without an associated PropertyDescriptor - consider the icon changed as well)
            if (e.ListChangedType == ListChangedType.ItemChanged &&
                (e.PropertyDescriptor == null || e.PropertyDescriptor.Name == "IconFile")) {

                DataGridViewRow row = PluginsDataGrid.Rows[e.NewIndex];
                Plugin rowPlugin = (Plugin) dataSource[e.NewIndex];
                DataGridViewImageCell rowIconCell = (DataGridViewImageCell) row.Cells[IconCol.Index];
                Debug.Assert(!(rowPlugin is SeparatorPlugin));
                LoadPluginIcon(rowPlugin, rowIconCell);
            }
        }
        
        /// <summary>
        /// Called when new rows are inserted in the plugins data grid. We use this
        /// opportunity to populate cells that cannot be populated through data binding.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void PluginsDataGrid_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
        {
            // The "Icon" column is not data-bound, because we can display two types
            // of images and this required setting a property of the cell that cannot
            // be controlled through data binding. We'll display icons here.
            for (int i = e.RowIndex; i < (e.RowIndex + e.RowCount); ++i) {
                DataGridViewRow row = PluginsDataGrid.Rows[i];
                Plugin rowPlugin = (Plugin) row.DataBoundItem;
                DataGridViewImageCell rowIconCell = (DataGridViewImageCell) row.Cells[IconCol.Index];
                bool setToolTip = false;
                if (!(rowPlugin is SeparatorPlugin)) {
                    if (rowPlugin is PipelinePlugin || rowPlugin is DefaultPlugin) {
                        setToolTip = true;
                    }
                    LoadPluginIcon(rowPlugin, rowIconCell);
                }

                // If the plugin's icon file can be edited, set tooltip text.
                // Otherwise, clear it and set the cell as read-only.
                if (AllowUserToEditIcons && setToolTip) {
                    rowIconCell.ToolTipText = Resources.PluginsListView_PluginsDataGrid_IconToolTipText;
                } else {
                    rowIconCell.ToolTipText = String.Empty;
                    rowIconCell.ReadOnly = true;
                }
            }
        }

        /// <summary>
        /// Called when a new plugin is selected in the data grid. We need to
        /// update our preview textbox.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void PluginsDataGrid_SelectionChanged(object sender, EventArgs e)
        {
            Debug.Assert(Settings != null);

            if (PluginsDataGrid.SelectedRows.Count == 1) {
                // Display preview of plugin.
                Plugin plugin = (Plugin) PluginsDataGrid.SelectedRows[0].DataBoundItem;
                if (plugin is SeparatorPlugin) {
                    // ...except for separators, which is pointless.
                    PreviewTxt.Clear();
                } else {
                    PreviewTxt.Text = plugin.GetPreview(Settings);
                }
            } else {
                // Clear content of preview textbox.
                PreviewTxt.Clear();
            }

            // Fire our own SelectionChanged event.
            SelectionChanged?.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        /// Called when the content of a cell is clicked in the plugins data grid.
        /// We need to handle clicks in icon cells to ask for an icon file.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void PluginsDataGrid_CellContentClick(object sender, DataGridViewCellEventArgs e)
        {
            // Immediately get current key modifiers to be able to determine the action to perform.
            Keys modifierKeys = Control.ModifierKeys & Keys.Modifiers;

            // We're only interested in clicks in icon cells.
            if (e.ColumnIndex == IconCol.Index && e.RowIndex != -1) {
                // Get plugin info.
                DataGridViewRow row = PluginsDataGrid.Rows[e.RowIndex];
                Plugin rowPlugin = (Plugin) row.DataBoundItem;
                DataGridViewImageCell rowIconCell = (DataGridViewImageCell) row.Cells[e.ColumnIndex];

                // Make sure this plugin's icon file is editable. We can't edit
                // icons of COM or separator plugins.
                if (AllowUserToEditIcons && !rowIconCell.ReadOnly) {
                    // Check action to perform.
                    bool iconUpdated = false;
                    if (modifierKeys == Keys.Shift) {
                        // Plugin must now use the default icon.
                        rowPlugin.IconFile = String.Empty;
                        iconUpdated = true;
                    } else if (modifierKeys == Keys.Control) {
                        // We must clear plugin's icon file.
                        rowPlugin.IconFile = null;
                        iconUpdated = true;
                    } else if (modifierKeys == Keys.None) {
                        // Ask user to provide a new icon file for this plugin. If the plugin
                        // already has an icon file, start in the same folder as the previous icon.
                        if (!String.IsNullOrEmpty(rowPlugin.IconFile)) {
                            ChoosePluginIconOpenDlg.InitialDirectory = Path.GetDirectoryName(rowPlugin.IconFile);
                        }
                        if (ChoosePluginIconOpenDlg.ShowDialog(this) == DialogResult.OK) {
                            // Load icon file for this plugin to update the grid.
                            rowPlugin.IconFile = ChoosePluginIconOpenDlg.FileName;
                            iconUpdated = true;
                        }
                    }

                    // Trigger our event if icon was updated.
                    if (iconUpdated) {
                        PluginIconChanged?.Invoke(this, EventArgs.Empty);
                    }
                }
            }
        }

        /// <summary>
        /// Called when the user moves the cursor inside a cell of the plugin
        /// data grid. We use this opportunity to modify the cursor.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void PluginsDataGrid_CellMouseEnter(object sender, DataGridViewCellEventArgs e)
        {
            // If the cursor enters an editable icon file cell, change the cursor to a hand.
            if (e.RowIndex >= 0 && e.ColumnIndex == IconCol.Index && AllowUserToEditIcons &&
                !PluginsDataGrid.Rows[e.RowIndex].Cells[e.ColumnIndex].ReadOnly) {

                PluginsDataGrid.Cursor = Cursors.Hand;
            } else {
                PluginsDataGrid.ResetCursor();
            }
        }

        /// <summary>
        /// Bean class storing information about a selected plugin.
        /// </summary>
        public sealed class SelectedPluginInfo
        {
            /// <summary>
            /// Reference to <see cref="Plugin"/> object.
            /// </summary>
            public Plugin Plugin
            {
                get;
                private set;
            }

            /// <summary>
            /// Index of plugin in the list view.
            /// </summary>
            public int Index
            {
                get;
                private set;
            }

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="plugin"><see cref="Plugin"/> object. Cannot be <c>null</c>.</param>
            /// <param name="index">Index of <paramref name="plugin"/> in the list view.</param>
            public SelectedPluginInfo(Plugin plugin, int index)
            {
                Debug.Assert(plugin != null);
                Debug.Assert(index >= 0);

                this.Plugin = plugin;
                this.Index = index;
            }
        }
    }
}
